/*
 * Copyright (c) 2025. felord.cn
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *       https://www.apache.org/licenses/LICENSE-2.0
 * Website:
 *       https://felord.cn
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.felord.retrofit.rxjava;

import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Maybe;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Scheduler;
import io.reactivex.rxjava3.core.Single;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import retrofit2.CallAdapter;
import retrofit2.HttpException;
import retrofit2.Response;
import retrofit2.Retrofit;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

/**
 * A {@linkplain CallAdapter.Factory call adapter} which uses RxJava 3 for creating observables.
 *
 * <p>Adding this class to {@link Retrofit} allows you to return an {@link Observable}, {@link
 * Flowable}, {@link Single}, {@link Completable} or {@link Maybe} from service methods.
 *
 * <pre><code>
 * interface MyService {
 *   &#64;GET("user/me")
 *   Observable&lt;User&gt; getUser()
 * }
 * </code></pre>
 * <p>
 * There are three configurations supported for the {@code Observable}, {@code Flowable}, {@code
 * Single}, {@link Completable} and {@code Maybe} type parameter:
 *
 * <ul>
 *   <li>Direct body (e.g., {@code Observable<User>}) calls {@code onNext} with the deserialized
 *       body for 2XX responses and calls {@code onError} with {@link HttpException} for non-2XX
 *       responses and {@link IOException} for network errors.
 *   <li>Response wrapped body (e.g., {@code Observable<Response<User>>}) calls {@code onNext} with
 *       a {@link Response} object for all HTTP responses and calls {@code onError} with {@link
 *       IOException} for network errors
 *   <li>Result wrapped body (e.g., {@code Observable<Result<User>>}) calls {@code onNext} with a
 *       {@link Result} object for all HTTP responses and errors.
 * </ul>
 */
public final class RxJava3SingleCallAdapterFactory extends CallAdapter.Factory {
    /**
     * Returns an instance which creates asynchronous observables that run on a background thread by
     * default. Applying {@code subscribeOn(..)} has no effect on instances created by the returned
     * factory.
     */
    public static RxJava3SingleCallAdapterFactory create() {
        return new RxJava3SingleCallAdapterFactory(null, true);
    }

    /**
     * Returns an instance which creates synchronous observables that do not operate on any scheduler
     * by default. Applying {@code subscribeOn(..)} will change the scheduler on which the HTTP calls
     * are made.
     */
    public static RxJava3SingleCallAdapterFactory createSynchronous() {
        return new RxJava3SingleCallAdapterFactory(null, false);
    }

    /**
     * Returns an instance which creates synchronous observables that {@code subscribeOn(..)} the
     * supplied {@code scheduler} by default.
     */
    @SuppressWarnings("ConstantConditions") // Guarding public API nullability.
    public static RxJava3SingleCallAdapterFactory createWithScheduler(Scheduler scheduler) {
        if (scheduler == null) throw new NullPointerException("scheduler == null");
        return new RxJava3SingleCallAdapterFactory(scheduler, false);
    }

    private final @Nullable Scheduler scheduler;
    private final boolean isAsync;

    private RxJava3SingleCallAdapterFactory(@Nullable Scheduler scheduler, boolean isAsync) {
        this.scheduler = scheduler;
        this.isAsync = isAsync;
    }

    @Override
    public @Nullable CallAdapter<?, ?> get(
            @NotNull Type returnType, @NotNull Annotation[] annotations, @NotNull Retrofit retrofit) {
        Class<?> rawType = getRawType(returnType);

        if (rawType == Completable.class) {
            // Completable is not parameterized (which is what the rest of this method deals with) so it
            // can only be created with a single configuration.
            return new RxJava3CallAdapter<>(
                    Void.class, scheduler, isAsync, false, true, false, false, false, true);
        }

        boolean isFlowable = rawType == Flowable.class;
        boolean isSingle = rawType == Single.class;
        boolean isMaybe = rawType == Maybe.class;
        if (rawType != Observable.class && !isFlowable && !isSingle && !isMaybe) {
            return null;
        }

        boolean isResult = false;
        boolean isBody = false;
        Type responseType;
        if (!(returnType instanceof ParameterizedType)) {
            String name =
                    isFlowable ? "Flowable" : isSingle ? "Single" : isMaybe ? "Maybe" : "Observable";
            throw new IllegalStateException(
                    name
                            + " return type must be parameterized"
                            + " as "
                            + name
                            + "<Foo> or "
                            + name
                            + "<? extends Foo>");
        }

        Type observableType = getParameterUpperBound(0, (ParameterizedType) returnType);
        Class<?> rawObservableType = getRawType(observableType);
        if (rawObservableType == Response.class) {
            if (!(observableType instanceof ParameterizedType)) {
                throw new IllegalStateException(
                        "Response must be parameterized" + " as Response<Foo> or Response<? extends Foo>");
            }
            responseType = getParameterUpperBound(0, (ParameterizedType) observableType);
        } else if (rawObservableType == Result.class) {
            if (!(observableType instanceof ParameterizedType)) {
                throw new IllegalStateException(
                        "Result must be parameterized" + " as Result<Foo> or Result<? extends Foo>");
            }
            responseType = getParameterUpperBound(0, (ParameterizedType) observableType);
            isResult = true;
        } else {
            responseType = observableType;
            isBody = true;
        }

        return new RxJava3CallAdapter<>(
                responseType, scheduler, isAsync, isResult, isBody, isFlowable, isSingle, isMaybe, false);
    }
}
